import 'package:analyzer/dart/element/element.dart';
import 'package:sqliteasy_builder/errors.dart';
import 'package:sqliteasy_core/annotations/database.dart';
import 'package:sqliteasy_core/core/sqliteasy_database.dart' hide ColumnInfo, IndexInfo, ForeignKeyInfo;
import '../database_util.dart';

import '../../model/database.dart';
import '../../model/code_fragment.dart';

class DatabaseCodeGenerator {
  static final databaseFieldName = "_db";
  static get masterTableName => sqliteasyMasterTableName;
  static get identityColumnName => sqliteasyIdentityColumnName;

  static CodeFragment gen(DatabaseInfo info, Iterable<MethodElement> daoGetters) {
    final superName = info.element.name;
    final className = DatabaseUtil.getDatabaseImplName(superName);

    final tableCreateFragment = _genTableCreation(info);
    final preMigrateFragment = _genPreMigrate(info);
    final postMigrateFragment = _genPostMigrate(info);
    final daoDefineFragment = _genDaoDefines(daoGetters);
    final daoGettersFragment = _genDaoGetters(daoGetters);
    final content = """
class ${className} extends ${superName} {
  @override
  int get version => ${info.version};
  @override
  String get identityHash => "${info.identityHash}";
  
  ${daoDefineFragment.content}

  @override
  void onCreate() {
    ${tableCreateFragment.content}
    ${_genMasterTable(info).content}
  }
  
  @override
  Future<SchemaValidateResult> validateSchema(SupportSQLiteDatabase db) async {
    ${_genSchemaValidateCode(info).content}
  }

  ${daoGettersFragment.content}
}
    """;
    List<String> imports = List();
    imports.add("package:sqliteasy_core/sqliteasy_core.dart");
    imports.add(info.element.location.components[0]);
    imports.addAll(daoGetters.map((e) => e.returnType.element.location.components[0]));
    return CodeFragment(content, imports);
  }

  static CodeFragment _genTableCreation(DatabaseInfo info) {
    final creationStatements = info.entities.map((entity) {
      bool hasAutoIncrementKey = false;
      final columnsStatement = entity.columns.map((column) {
        if (column.autoGenerate == true) {
          hasAutoIncrementKey = true;
        }
        return _genTableCreationColumnStatement(column);
      }).join(",");
      final primaryKeyStatement =
      hasAutoIncrementKey ? "" : ",PRIMARY KEY (${entity.primaryKeys.map((e) => e.name).join(",")})";
      final foreignKeyStatement = entity.foreignKeys?.isNotEmpty == true ? ",${_genForeignKeyStatement(entity,
          entity.foreignKeys)}" : "";
      final sqlStatement =
          "CREATE TABLE IF NOT EXISTS `${entity
          .tableName}` ($columnsStatement$primaryKeyStatement$foreignKeyStatement)";
      return """exec("$sqlStatement");""";
    }).join("\n");
    final indicesStatements = info.entities.map((entity) {
      return entity.indices.map((e) => """exec("${_genIndexCreationStatement(entity, e)}");""").join("\n");
    }).join("\n");
    return CodeFragment(creationStatements + "\n" + indicesStatements);
  }

  static CodeFragment _genMasterTable(DatabaseInfo info) {
    return CodeFragment("""
    exec("CREATE TABLE IF NOT EXISTS ${masterTableName} (id INTEGER PRIMARY KEY,${identityColumnName} TEXT)");
    exec("INSERT OR REPLACE INTO ${masterTableName} (id, ${identityColumnName}) VALUES(${sqliteasyIdentityColumnDefaultValue}, '\${identityHash}')");
    """);
  }

  static String _genIndexCreationStatement(EntityInfo entityInfo, IndexInfo index) {
    // check
    final columnsSet = entityInfo.columns.map((e) => e.name).toSet();
    final columnMap = Map<String, ColumnInfo>();
    entityInfo.columns.forEach((element) {
      columnMap[element.field.name] = element;
    });
    if (index.columns?.isNotEmpty != true) {
      throw SQLiteasyBuildError("the columns of Index can not be empty");
    }
    final columns = List();
    index.columns.forEach((element) {
      if (columnsSet.contains(element)) {
        columns.add(element);
      } /*else if (columnMap.containsKey(element)) {
        columns.add(columnMap[element].name);
      }*/ else {
        throw SQLiteasyBuildError("the column name ${element} in Index ${index.name} of table ${entityInfo.tableName} is not existed.", entityInfo.location);
      }
    });
    return "CREATE ${index.unique == true ? "UNIQUE" : ""} INDEX IF NOT EXISTS " +
        "`${index.name ?? DatabaseUtil.getDefaultIndexName(entityInfo.tableName, index.columns)}` " +
        "ON `${entityInfo.tableName}` (${columns.map((e) => "`$e`").join(",")})";
  }

  static String _genForeignKeyStatement(EntityInfo entityInfo, List<ForeignKeyInfo> foreignKeys) {
    StringBuffer sb = StringBuffer();
    final columnSet = entityInfo.columns.map((e) => e.name).toSet();
    for (var e in foreignKeys) {
      StringBuffer columnStrBuilder = StringBuffer();
      e.columns.forEach((element) {
        if (!columnSet.contains(element)) {
          throw SQLiteasyBuildError("the column ${element} in the ForeignKey of table ${entityInfo.tableName} is not existed.");
        }
        columnStrBuilder.write("`$element`");
        columnStrBuilder.write(",");
      });
      String columnStr = columnStrBuilder.toString();

      final refEntityInfo = e.entity;
      final refColumnSet = refEntityInfo.columns.map((e) => e.name).toSet();
      StringBuffer refColumnStrBuilder = StringBuffer();
      e.referenceColumns.forEach((element) {
        if (!refColumnSet.contains(element)) {
          throw SQLiteasyBuildError("the column ${element} in the ForeignKey of table ${refEntityInfo.tableName} is not existed.");
        }
        refColumnStrBuilder.write("`$element`");
        refColumnStrBuilder.write(",");
      });
      String refColumnStr = refColumnStrBuilder.toString();

      sb.write(
      "FOREIGN KEY(${columnStr.substring(0, columnStr.length - 1)}) REFERENCES `${e.entity.tableName}`(${refColumnStr.substring(0, refColumnStr.length - 1)}) ON UPDATE ${_getActionString(e.onUpdate)} ON DELETE ${_getActionString(e.onDelete)} ${e.deferred == true ? "DEFERRABLE INITIALLY DEFERRED" : ""}");
      sb.write(",");
    }
    var result = sb.toString();
    result = result.substring(0, result.length - 1);
    return result;
  }

  static CodeFragment _genSchemaValidateCode(DatabaseInfo info) {
    StringBuffer sb = StringBuffer();
    info.entities?.forEach((entity) {
      final columnSetVarName = getVarName("${entity.entityName}Columns");
      sb.writeln("final ${columnSetVarName} = Set<ColumnInfo>();");
      entity.columns?.forEach((element) {
        sb.writeln("""${columnSetVarName}.add(ColumnInfo(
          name: "${element.name}",
          type: SQLiteTypes.getTypeFromName("${element.type}"),
          notNull: ${element.notNull ?? false},
          defaultValue: ${element.defaultValue == null ? "null" : '"${element.defaultValue}"'},
          primaryKeyPosition: ${element.primaryKeyPosition != null ? (element.primaryKeyPosition + 1) : 0},
        ));""");
      });

      sb.writeln("\n");
      final foreignKeySetVarName = getVarName("${entity.entityName}ForeignKeys");
      sb.writeln("final ${foreignKeySetVarName} = Set<ForeignKeyInfo>();");
      entity.foreignKeys?.forEach((element) {
        sb.writeln("""${foreignKeySetVarName}.add(ForeignKeyInfo(
          referenceTable: "${element.entity.tableName}",
          onUpdate: ${element.onUpdate ?? Action.NO_ACTION},
          onDelete: ${element.onDelete ?? Action.NO_ACTION},
          columns: [${element.columns.map((e) => '"${e}"').join(",")}],
          referenceColumns: [${element.referenceColumns.map((e) => '"${e}"').join(",")}],
        ));""");
      });

      sb.writeln("\n");
      final indexSetVarName = getVarName("${entity.entityName}Indexes");
      sb.writeln("final ${indexSetVarName} = Set<IndexInfo>();");
      entity.indices?.forEach((element) {
        sb.writeln("""${indexSetVarName}.add(IndexInfo(
          name: "${element.name}",
          unique: ${element.unique ?? false},
          columns: [${element.columns.map((e) => '"$e"').join(",")}],
        ));""");
      });

      sb.writeln("\n");
      final tableInfoVarName = getVarName("${entity.entityName}TableInfo");
      sb.writeln("""final ${tableInfoVarName} = TableInfo(
        name: "${entity.tableName}", 
        columns: ${columnSetVarName}, 
        foreignKeys: ${foreignKeySetVarName},
        indexes: ${indexSetVarName},
      );""");

      final existedInfoVarName = getVarName("${entity.entityName}ExistedInfo");
      sb.writeln("""final ${existedInfoVarName} = await TableInfo.read(db, "${entity.tableName}");""");
      sb.writeln("""
      if (${existedInfoVarName} != null && ${existedInfoVarName} != ${tableInfoVarName}) {
        return SchemaValidateResult(isValid: false, errorMsg: "${entity.tableName}(${entity.location}).\\nExpected:\\n\${${tableInfoVarName}}\\nFound:\\n\${${existedInfoVarName}}");
      }
      """);
    });
    sb.writeln("return SchemaValidateResult(isValid: true);");
    return CodeFragment(sb.toString());
  }

  static final _actionStringMap = {
    Action.NO_ACTION: "NO ACTION",
    Action.CASCADE: "CASCADE",
    Action.RESTRICT: "RESTRICT",
    Action.SET_DEFAULT: "SET DEFAULT",
    Action.SET_NULL: "SET NULL",
  };

  static String _getActionString(Action action) {
    return _actionStringMap[action ?? Action.NO_ACTION];
  }

  static String _genTableCreationColumnStatement(ColumnInfo column) {
    String primaryKeyStatement = column.autoGenerate == true ? " PRIMARY KEY AUTOINCREMENT" : "";
    return "`${column.name}` ${column.type}${primaryKeyStatement}";
  }

  static CodeFragment _genPreMigrate(DatabaseInfo info) {
    return CodeFragment("");
  }

  static CodeFragment _genPostMigrate(DatabaseInfo info) {
    return CodeFragment("");
  }

  static CodeFragment _genDaoGetters(Iterable<MethodElement> daoGetters) {
    return CodeFragment(daoGetters.map((e) {
      String fieldName = getFieldNameOfClass(e.returnType.element.name);
      return """
  @override
  ${e.returnType.element.name} ${e.name}() {
    if (${fieldName} == null) {
      ${fieldName} = ${DatabaseUtil.getDaoImplName(e.returnType.element)}(this);
    }
    return ${fieldName}!;
  }
    """;
    }).join("\n"));
  }

  static CodeFragment _genDaoDefines(Iterable<MethodElement> daoGetters) {
    Set<String> daoDefSet = Set();
    for (var e in daoGetters) {
      daoDefSet.add("${e.returnType.element.name}? ${getFieldNameOfClass(e.returnType.element.name)};");
    }
    return CodeFragment(daoDefSet.join());
  }

  static String getFieldNameOfClass(String className) {
    if (className.isEmpty) {
      return "";
    }
    return "_" + className[0].toLowerCase() + className.substring(1);
  }

  static String getVarName(String name) {
    if ((name?.length ?? 0) == 0) {
      return name;
    }
    return name[0].toLowerCase() + name.substring(1);
  }
}
